/**
 * Copyright (c) 2011 Roundarch (www.roundarch.com)
 *
 * Author Joshua Jung
 *
 * ---
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.roundarch.flex.controls
{
    import com.roundarch.flex.controls.graphControlClasses.GraphControlAxisOptions;
    import com.roundarch.flex.controls.graphControlClasses.GraphControlScrollType;
    import com.roundarch.flex.controls.graphControlClasses.GraphControlUtil;
    import com.roundarch.flex.controls.graphControlClasses.events.GraphControlSelectorEvent;
    import com.roundarch.flex.controls.graphControlInterfaces.IGraphControlChild;
    import com.roundarch.flex.controls.graphControlInterfaces.IGraphControlOverlay;
    import com.roundarch.flex.controls.graphControlInterfaces.IGraphControlRenderer;
    import com.roundarch.flex.controls.graphControlInterfaces.IGraphControlSelector;
    import com.roundarch.flex.controls.graphControlInterfaces.IGraphControlSelectorPositioned;
    import com.roundarch.flex.controls.graphControlInterfaces.IGraphControlUnderlay;

    import flash.display.DisplayObject;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.Point;
    import flash.geom.Rectangle;

    import mx.collections.ArrayCollection;
    import mx.core.Container;
    import mx.core.IUIComponent;
    import mx.core.ScrollPolicy;
    import mx.core.UIComponent;

    [Event(name = "viewportChanged")]
    /**
     * @author
     */
    public class GraphControlBase extends Container
    {
        //---------------------------------------------------------------------
        //
        //  Variables
        //
        //---------------------------------------------------------------------
        /**
         * The top renderer. Used for tint, glare, etc.
         */
        protected var overlay : IGraphControlOverlay;

        /**
         * The renderers of this graph control.
         *
         * @default []
         */
        protected var renderers : Array = [];

        protected var selectorDisplayOrder : ArrayCollection = new ArrayCollection();

        /**
         * The value adjusting controls of this graph control (probably draggable buttons in
         * most cases).
         *
         * @default new ArrayCollection()
         */
        protected var selectors : ArrayCollection = new ArrayCollection();

        /**
         * The bottom renderer. Used for background, border, etc.
         */
        protected var underlay : IGraphControlUnderlay;

        //---------------------------------------------------------------------
        //
        //  Properties
        //
        //---------------------------------------------------------------------
        //-----------------------------
        // dataProvider
        //-----------------------------
        /**
         * @private
         */
        protected var _dataProvider : Object;

        [Bindable("dataProviderChanged")]
        /**
         * The dataProvider for this graph control.
         */
        public function get dataProvider() : Object
        {
            return _dataProvider;
        }

        /**
         * @private
         */
        public function set dataProvider(value : Object) : void
        {
            if (value == _dataProvider)
            {
                return;
            }

            if (!value is ArrayCollection)
            {
                throw new Error("GraphControl dataProvider can only be ArrayCollection at this time.");
            }

            _dataProvider = value;

            dispatchEvent(new Event("dataProviderChanged"));
        }

        //-----------------------------
        // primarySelector
        //-----------------------------
        /**
         * @private
         */
        protected var _primarySelector : * = "closest";

        [Bindable("primarySelectorChanged")]
        /**
         * When set, if the user clicks on the graph this selector moves to that location.
         *
         * Can be a IGraphControlSelector or 'closest'.
         *
         * @default 'closest'
         */
        public function get primarySelector() : *
        {
            return _primarySelector;
        }

        /**
         * @private
         */
        public function set primarySelector(value : *) : void
        {
            if (value == _primarySelector)
            {
                return;
            }

            _primarySelector = value;

            dispatchEvent(new Event("primarySelectorChanged"));
        }

        //-----------------------------
        // verticalAxisOptions
        //-----------------------------
        /**
         * @private
         */
        protected var _verticalAxisOptions : GraphControlAxisOptions = null;

        /**
         * @private
         */
        protected var verticalAxisOptionsChanged : Boolean = false;

        [Bindable("verticalAxisOptionsChanged")]
        /**
         * The vertical axis properties
         */
        public function get verticalAxisOptions() : GraphControlAxisOptions
        {
            return _verticalAxisOptions;
        }

        /**
         * @private
         */
        public function set verticalAxisOptions(value : GraphControlAxisOptions) : void
        {
            if (value == _verticalAxisOptions)
            {
                return;
            }

            _verticalAxisOptions = value;

            _verticalAxisOptions.addEventListener("minMaxChanged", verticalAxisOptions_minMaxChanged);
            _verticalAxisOptions.addEventListener("visibleMinMaxChanged", verticalAxisOptions_visibleMinMaxChanged);

            verticalAxisOptionsChanged = true;

            invalidateProperties();

            dispatchEvent(new Event("verticalAxisOptionsChanged"));
        }

        //-----------------------------
        // horizontalAxisOptions
        //-----------------------------
        /**
         * @private
         */
        protected var _horizontalAxisOptions : GraphControlAxisOptions = null;

        /**
         * @private
         */
        protected var horizontalAxisOptionsChanged : Boolean = false;

        [Bindable("horizontalAxisOptionsChanged")]
        /**
         * The horizontal axis properties
         */
        public function get horizontalAxisOptions() : GraphControlAxisOptions
        {
            return _horizontalAxisOptions;
        }

        /**
         * @private
         */
        public function set horizontalAxisOptions(value : GraphControlAxisOptions) : void
        {
            if (value == _horizontalAxisOptions)
            {
                return;
            }

            _horizontalAxisOptions = value;

            _horizontalAxisOptions.addEventListener("minMaxChanged", horizontalAxisOptions_minMaxChanged);
            _horizontalAxisOptions.addEventListener("visibleMinMaxChanged", horizontalAxisOptions_visibleMinMaxChanged);

            horizontalAxisOptionsChanged = true;

            invalidateProperties();

            dispatchEvent(new Event("horizontalAxisOptionsChanged"));
        }

        //-----------------------------
        // selectedPoints
        //-----------------------------
        /**
         * @private
         */
        protected var _selectedPoints : Array = [];

        /**
         * @private
         */
        protected var selectedPointsChanged : Boolean;

        [Bindable("selectedPointsChanged")]
        /**
         * The selected values on the graph.
         */
        public function get selectedPoints() : Array
        {
            return _selectedPoints;
        }

        /**
         * @private
         */
        public function set selectedPoints(value : Array) : void
        {
            if (value == _selectedPoints && !value)
            {
                return;
            }

            _selectedPoints = value;

            selectedPointsChanged = true;

            invalidateProperties();

            dispatchEvent(new Event("selectedPointsChanged"));
        }

        //-----------------------------
        // padding
        //-----------------------------
        /**
         * @private
         */
        protected var _padding : Rectangle = new Rectangle(0, 0, 0, 0);

        [Bindable("paddingChanged")]
        /**
         *
         *
         * @default null
         */
        public function get padding() : Rectangle
        {
            return _padding;
        }

        /**
         * @private
         */
        public function set padding(value : Rectangle) : void
        {
            if (value == _padding)
            {
                return;
            }

            _padding = value;

            invalidateDisplayList();

            dispatchEvent(new Event("paddingChanged"));
        }

        //---------------------------------------------------------------------
        //
        //  Constructor
        //
        //---------------------------------------------------------------------
        /**
         *  Constructor
         */
        public function GraphControlBase()
        {
            super();

            horizontalScrollPolicy = ScrollPolicy.OFF;
            verticalScrollPolicy = ScrollPolicy.OFF;

            clipContent = false;

            addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
            addEventListener(MouseEvent.CLICK, mouseClickHandler);
        }

        //---------------------------------------------------------------------
        //
        //  Methods
        //
        //---------------------------------------------------------------------
        public function getPointForMouse(mouseX : Number, mouseY : Number, stageMouseCoordinates : Boolean = false) : Point
        {
            if (stageMouseCoordinates)
            {
                var p : Point = globalToLocal(new Point(mouseX, mouseY));

                return GraphControlUtil.getValueforScreen(p, this);
            }

            return GraphControlUtil.getValueforScreen(new Point(mouseX, mouseY), this);
        }

        [Bindable("selectedPointsChanged")]
        public function getSelectedPointFor(selector : IGraphControlSelector) : Point
        {
            return _selectedPoints[getSelectorIndex(selector)];
        }

        public function getSelectorIndex(selector : IGraphControlSelector) : int
        {
            return selectors.getItemIndex(selector);
        }

        public function isValid(value : Point) : Boolean
        {
            return (value.x >= horizontalAxisOptions.minMax.start && value.x <= horizontalAxisOptions.minMax.end) || (value.x >= horizontalAxisOptions.minMax.start && value.x < horizontalAxisOptions.minMax.end);
        }

        public function isVisible(value : Point) : Boolean
        {
            return value && value.x >= horizontalAxisOptions.visibleMinMax.start && value.x <= horizontalAxisOptions.visibleMinMax.end;
        }

        public function requestNewSelectorValue(selector : IGraphControlSelector, newValueRequest : Point, suppressNotifications : Boolean = false) : Boolean
        {
            if (newValueRequest)
            {
                // We want to dupe the Point just in case it is the same one that already exists in the selectedPoints.
                var newValue : Point = new Point(newValueRequest.x, newValueRequest.y);

                // If this selector has a discrete interval, we "snap to" the start value
                // of the interval selected.
                if (selector.hInterval && selector.hInterval.ready)
                {
                    newValue.x = selector.hInterval.getIntervalFor(newValue.x).start;
                }

                if (selector.vInterval && selector.vInterval.ready)
                {
                    newValue.y = selector.vInterval.getIntervalFor(newValue.y).start;
                }

                if (newValue.x < horizontalAxisOptions.minMax.start)
                {
                    newValue.x = horizontalAxisOptions.minMax.start;
                }

                if (newValue.x > horizontalAxisOptions.minMax.end)
                {
                    newValue.x = horizontalAxisOptions.minMax.end;
                }

                if (newValue.y < verticalAxisOptions.minMax.start)
                {
                    newValue.y = verticalAxisOptions.minMax.start;
                }

                if (newValue.y > verticalAxisOptions.minMax.end)
                {
                    newValue.y = verticalAxisOptions.minMax.end;
                }

                var selectorIndex : int = selectors.getItemIndex(selector);

                var newValues : Array = copyValues();

                if (selectorIndex >= newValues.length)
                {
                    // THis is a new selector and we don't have a value for it yet!   
                }
                else if (newValues[selectorIndex].x == newValue.x && newValues[selectorIndex].y == newValue.y)
                {
                    // The selector already has this value, so don't do anything.
                    return false;
                }

                trace("updating value");

                // We must create a new point object. The reason is that for scrolling the same point object is used to keep track of the
                // scrolling start position and then that same object is used over and over. If we use that Point object, then every time we 
                // come into this method we duplicate the current values into newValues (above), but the scrolling method has already updated
                // the point and then it thinks they are the same and doesn't do anything.
                newValues[selectorIndex] = new Point(newValue.x, newValue.y);

                selectedPoints = newValues;

                selector.selectedPointChanged();

                if (!suppressNotifications)
                {
                    dispatchEvent(new Event("selectedPointsChanged"));
                    invalidateVisibleArea();
                    invalidateDisplayList();
                }

                return true;
            }

            return false;
        }

        public function requestRefreshSelectorValue(selector : IGraphControlSelector, suppressNotifications : Boolean = false) : Boolean
        {
            return requestNewSelectorValue(selector, getSelectedPointFor(selector), suppressNotifications);
        }

        public function requestRefreshSelectors() : void
        {
            var changeOccurred : Boolean = false;

            for each (var selector : IGraphControlSelector in selectors)
            {
                changeOccurred = requestRefreshSelectorValue(selector, true) ? true : changeOccurred;
            }

            if (changeOccurred)
            {
                dispatchEvent(new Event("selectedPointsChanged"));
                invalidateVisibleArea();
                invalidateDisplayList();
            }
        }

        public function setSelectorFocus(selector : IGraphControlSelector) : void
        {
            var oldIx : int = selectorDisplayOrder.getItemIndex(selector);

            selectorDisplayOrder.addItem(selector);

            selectorDisplayOrder.removeItemAt(oldIx);

            // This will resort the children in the display list.
            invalidateProperties();
        }

        protected function copyValues() : Array
        {
            if (!selectedPoints)
            {
                return null;
            }

            var copy : Array = new Array(_selectedPoints.length);

            for (var i : int = 0; i < _selectedPoints.length; i++)
            {
                if (!_selectedPoints[i])
                {
                    _selectedPoints[i] = new Point(Number.MIN_VALUE, Number.MIN_VALUE);
                }

                copy[i] = new Point(_selectedPoints[i].x, _selectedPoints[i].y);
            }

            return copy;
        }

        /**
         * Invalidates all the renderers, underlay, and overlay properties and display list,
         * forcing them to redraw themselves.
         */
        public function invalidateAll() : void
        {
            invalidateProperties();
            invalidateDisplayList();

            for (var i : int = 0; i < renderers.length; i++)
            {
                (renderers[i] as UIComponent).invalidateProperties();
                (renderers[i] as UIComponent).invalidateDisplayList();
            }

            if (underlay)
            {
                (underlay as UIComponent).invalidateProperties();
                (underlay as UIComponent).invalidateDisplayList();
            }

            if (overlay)
            {
                (overlay as UIComponent).invalidateProperties();
                (overlay as UIComponent).invalidateDisplayList();
            }
        }

        public function invalidateAxis() : void
        {
            requestRefreshSelectors();

            invalidateVisibleArea();

            invalidateAll();
        }

        /**
         * Invalidates all the renderers properties and display list,
         * forcing them to redraw themselves.
         */
        public function invalidateRenderers() : void
        {
            for (var i : int = 0; i < renderers.length; i++)
            {
                (renderers[i] as UIComponent).invalidateProperties();
                (renderers[i] as UIComponent).invalidateDisplayList();
            }
        }

        public function invalidateVisibleArea() : void
        {
            var selector : IGraphControlSelector;
            var p : Point;

            var vpc : Boolean = false;

            // Handle all selectors that want us to RESIZE based on their position
            for each (selector in selectors)
            {
                if (selector.scrollType == GraphControlScrollType.RESIZE)
                {
                    p = selector.value;

                    if (p.x < horizontalAxisOptions.visibleMinMax.start)
                    {
                        trace("adjusting visible area");
                        horizontalAxisOptions.visibleMinMax.start = p.x;
                        invalidateRenderers();
                    }
                    else if (p.x > horizontalAxisOptions.visibleMinMax.end)
                    {
                        horizontalAxisOptions.visibleMinMax.end = p.x;
                        invalidateRenderers();
                    }

                    if (p.y < verticalAxisOptions.visibleMinMax.start)
                    {
                        verticalAxisOptions.visibleMinMax.start = p.y;
                        invalidateRenderers();
                    }
                    else if (p.y > verticalAxisOptions.visibleMinMax.end)
                    {
                        verticalAxisOptions.visibleMinMax.end = p.y;
                        invalidateRenderers();
                    }
                }
            }

            // Handle all selectors that want us to MOVE based on their position
            for each (selector in selectors)
            {
                if (selector.scrollType == GraphControlScrollType.MOVE)
                {
                    p = selector.value;

                    if (p.x < horizontalAxisOptions.visibleMinMax.start)
                    {
                        var len : Number = horizontalAxisOptions.visibleMinMax.length;
                        horizontalAxisOptions.visibleMinMax.start = p.x;
                        horizontalAxisOptions.visibleMinMax.end = p.x + len;
                        invalidateRenderers();
                    }
                    else if (p.x > horizontalAxisOptions.visibleMinMax.end)
                    {
                        var len : Number = horizontalAxisOptions.visibleMinMax.length;
                        horizontalAxisOptions.visibleMinMax.end = p.x;
                        horizontalAxisOptions.visibleMinMax.start = p.x - len;
                        invalidateRenderers();
                    }

                    if (p.y < verticalAxisOptions.visibleMinMax.start)
                    {
                        var len : Number = verticalAxisOptions.visibleMinMax.length;
                        verticalAxisOptions.visibleMinMax.start = p.y;
                        verticalAxisOptions.visibleMinMax.end = p.y + len;
                        invalidateRenderers();
                    }
                    else if (p.y > verticalAxisOptions.visibleMinMax.end)
                    {
                        var len : Number = verticalAxisOptions.visibleMinMax.length;
                        verticalAxisOptions.visibleMinMax.end = p.y;
                        verticalAxisOptions.visibleMinMax.start = p.y - len;
                        invalidateRenderers();
                    }
                }
            }
        }

        //---------------------------------------------------------------------
        //
        //  Overridden Methods
        //
        //---------------------------------------------------------------------
        /**
         * @inheritDoc
         */
        override public function setChildIndex(child : DisplayObject, newIndex : int) : void
        {
            var oldIndex : int = getChildIndex(child);

            if (newIndex == oldIndex)
            {
                return;
            }

            super.setChildIndex(child, newIndex);
        }

        /**
         * @inheritDoc
         */
        override protected function commitProperties() : void
        {
            super.commitProperties();

            var i : int;

            if (!selectedPoints && selectors.length)
            {
                selectedPoints = new Array(selectors.length);

                for (i = 0; i < selectors.length; i++)
                {
                    selectedPoints[i] = new Point(isNaN(horizontalAxisOptions.minMax.start) ? 0 : horizontalAxisOptions.minMax.start, 0);
                }

                invalidateDisplayList();
            }

            if (selectedPointsChanged)
            {
                selectedPointsChanged = false;

                requestRefreshSelectors();

                invalidateDisplayList();
            }

            if (horizontalAxisOptionsChanged)
            {
                horizontalAxisOptionsChanged = false;

                horizontalAxisOptions_minMaxChanged(null);
            }

            // Organize our children
            // TODO this should only be done after a new child is added and needs to
            // take into account a selectors placeBehindRenderers property.
            if (underlay)
            {
                setChildIndex(underlay as DisplayObject, 0);
            }

            for (i = 0; i < renderers.length; i++)
            {
                setChildIndex(renderers[i] as DisplayObject, i + 1);
            }

            var selectorsBehindRenderers : int = 0;

            for (i = 0; i < selectorDisplayOrder.length; i++)
            {
                setChildIndex(selectorDisplayOrder[i] as DisplayObject, (underlay ? 1 : 0) + renderers.length + i);
            }

            if (overlay)
            {
                setChildIndex(overlay as DisplayObject, numChildren - 1);
            }
        }

        /**
         * @inheritDoc
         */
        override public function addChild(child : DisplayObject) : DisplayObject
        {
            if (child is IGraphControlUnderlay)
            {
                if (underlay)
                {
                    removeChild(underlay as DisplayObject);
                }

                underlay = child as IGraphControlUnderlay;

                if (child is IGraphControlChild)
                {
                    (child as IGraphControlChild).parentGC = this;
                }

                return addChildAt(child, 0);
            }

            if (child is IGraphControlOverlay)
            {
                if (overlay)
                {
                    removeChild(overlay as DisplayObject);
                }

                overlay = child as IGraphControlOverlay;
            }

            if (child is IGraphControlRenderer)
            {
                renderers.push(child);
            }

            if (child is IGraphControlSelector)
            {
                selectorDisplayOrder.addItem(child);
                selectors.addItem(child);
            }

            if (child is IGraphControlChild)
            {
                (child as IGraphControlChild).parentGC = this;
            }

            invalidateProperties();

            return super.addChild(child);
        }

        /**
         * @inheritDoc
         */
        override protected function updateDisplayList(unscaledWidth : Number, unscaledHeight : Number) : void
        {
            super.updateDisplayList(unscaledWidth, unscaledHeight);

            // This is to ensure that hitTestPoint still works for this control. We draw an almost completely transparent
            // rectangle to ensure that all are within this graph control registers as a hit point.
            graphics.clear();
            graphics.beginFill(0xFFFFFF, 0.01);
            graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
            graphics.endFill();

            var selector : IGraphControlSelector;

            // Size all renderers exactly to our own size.
            for (var r : int = 0; r < numChildren; r++)
            {
                if (getChildAt(r) is IGraphControlSelector)
                {
                    selector = getChildAt(r) as IGraphControlSelector;

                    selector.setActualSize(selector.measuredWidth, selector.measuredHeight);
                }
                else if (getChildAt(r) is IGraphControlChild)
                {
                    (getChildAt(r) as IUIComponent).setActualSize(unscaledWidth, unscaledHeight);
                    (getChildAt(r) as IUIComponent).move(0, 0);
                }
            }

            if (selectors)
            {
                // Update the display on all of our selectors.
                // TODO get this to work with vertical values.
                for (var i : int = 0; i < selectors.length; i++)
                {
                    selector = selectors[i] as IGraphControlSelector;
                    var sp : IGraphControlSelectorPositioned = selector as IGraphControlSelectorPositioned;

                    selector.visible = selector.alwaysVisible || isVisible(selector.value);

                    if (sp && sp.autoPosition && selector.visible)
                    {
                        var screen : Point = GraphControlUtil.getScreenforValue(selector.value, this);

                        selector.move(screen.x, screen.y + selector.offsetY);
                    }
                }
            }
        }

        protected function findClosestSelector(p : Point) : IGraphControlSelector
        {
            var closest : IGraphControlSelector = null;
            var closestDis : Number = Number.MAX_VALUE;

            for each (var s : IGraphControlSelector in selectors)
            {
                var dis : Number = Math.sqrt((s.value.x - p.x) * (s.value.x - p.x) + (s.value.y - p.y) * (s.value.y - p.y));

                if (dis < closestDis)
                {
                    closest = s;
                    closestDis = dis;
                }
            }

            return closest;
        }

        //---------------------------------------------------------------------
        //
        //  Events
        //
        //---------------------------------------------------------------------
        /**
         * @private
         */
        protected function horizontalAxisOptions_minMaxChanged(event : Event) : void
        {
            requestRefreshSelectors();
        }

        /**
         * @private
         */
        protected function horizontalAxisOptions_visibleMinMaxChanged(event : Event) : void
        {
            invalidateDisplayList();

            dispatchEvent(new Event("viewportChanged"));
        }

        /**
         * @private
         */
        protected function verticalAxisOptions_minMaxChanged(event : Event) : void
        {
            requestRefreshSelectors();
        }

        /**
         * @private
         */
        protected function verticalAxisOptions_visibleMinMaxChanged(event : Event) : void
        {
            invalidateDisplayList();

            dispatchEvent(new Event("viewportChanged"));
        }

        /**
         * @private
         */
        protected function addedToStageHandler(event : Event) : void
        {
        }

        /**
         * @private
         */
        protected function mouseClickHandler(event : MouseEvent) : void
        {
            if (primarySelector)
            {
                var p : Point = new Point(event.stageX, event.stageY);

                p = globalToLocal(p);

                if (p.x > 0 && p.x < width && p.y > 0 && p.y < height)
                {
                    p = GraphControlUtil.getValueforScreen(p, this);

                    if (primarySelector == "closest")
                    {
                        var selector : IGraphControlSelector = findClosestSelector(p);

                        selector.invalidateValue(p);
                    }
                    else if (primarySelector is IGraphControlSelector)
                    {
                        primarySelector.invalidateValue(p);
                    }
                }
            }
        }
    }
}
